<!DOCTYPE html>
<html lang="en">

<!-- Head tag -->
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="google-site-verification" content="xBT4GhYoi5qRD5tr338pgPM5OWHHIDR6mNg1a3euekI" />
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="keyword"  content="">
    <link rel="shortcut icon" href="/img/favicon.ico">

    <title>
        
          使用HeadlessChrome做单页应用SEO - 浩麟的博客
        
    </title>

    <link rel="canonical" href="https://wuhaolin.cn/2017/06/27/使用HeadlessChrome做单页应用SEO/">

    <!-- Bootstrap Core CSS -->
    
<link rel="stylesheet" href="/css/bootstrap.min.css">


    <!-- Custom CSS -->
    
<link rel="stylesheet" href="/css/hux-blog.min.css">


    <!-- Pygments Highlight CSS -->
    
<link rel="stylesheet" href="/css/highlight.css">


    <!-- Custom Fonts -->
    <!-- <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css"> -->
    <!-- Hux change font-awesome CDN to qiniu -->
    <link href="https://cdn.staticfile.org/font-awesome/4.5.0/css/font-awesome.min.css" rel="stylesheet" type="text/css">


    <!-- Hux Delete, sad but pending in China
    <link href='http://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic' rel='stylesheet' type='text/css'>
    <link href='http://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800' rel='stylesheet' type='text/
    css'>
    -->


    <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
        <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
        <script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
    <![endif]-->

    <!-- ga & ba script hoook -->
    <script></script>
<meta name="generator" content="Hexo 4.1.1"></head>


<!-- hack iOS CSS :active style -->
<body ontouchstart="">

<!-- Navigation -->
<nav class="navbar navbar-default navbar-custom navbar-fixed-top">
    <div class="container-fluid">
        <!-- Brand and toggle get grouped for better mobile display -->
        <div class="navbar-header page-scroll">
            <button type="button" class="navbar-toggle">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="/">浩麟的博客</a>
        </div>

        <!-- Collect the nav links, forms, and other content for toggling -->
        <!-- Known Issue, found by Hux:
            <nav>'s height woule be hold on by its content.
            so, when navbar scale out, the <nav> will cover tags.
            also mask any touch event of tags, unfortunately.
        -->
        <div id="huxblog_navbar">
            <div class="navbar-collapse">
                <ul class="nav navbar-nav navbar-right">
                    <li>
                        <a href="/">Home</a>
                    </li>
                    <li>
                        <a href="https://github.com/gwuhaolin/blog/" target="_blank">订阅</a>
                    </li>

                    

                        
                    

                        
                        <li>
                            <a href="/about/">关于</a>
                        </li>
                        
                    

                        
                        <li>
                            <a href="/archives/">归档</a>
                        </li>
                        
                    

                        
                        <li>
                            <a href="/tags/">标签</a>
                        </li>
                        
                    

                </ul>
            </div>
        </div>
        <!-- /.navbar-collapse -->
    </div>
    <!-- /.container -->
</nav>
<script>
    // Drop Bootstarp low-performance Navbar
    // Use customize navbar with high-quality material design animation
    // in high-perf jank-free CSS3 implementation
    var $body   = document.body;
    var $toggle = document.querySelector('.navbar-toggle');
    var $navbar = document.querySelector('#huxblog_navbar');
    var $collapse = document.querySelector('.navbar-collapse');

    $toggle.addEventListener('click', handleMagic)
    function handleMagic(e){
        if ($navbar.className.indexOf('in') > 0) {
        // CLOSE
            $navbar.className = " ";
            // wait until animation end.
            setTimeout(function(){
                // prevent frequently toggle
                if($navbar.className.indexOf('in') < 0) {
                    $collapse.style.height = "0px"
                }
            },400)
        }else{
        // OPEN
            $collapse.style.height = "auto"
            $navbar.className += " in";
        }
    }
</script>


<!-- Main Content -->

<!-- Image to hack wechat -->
<!-- <img src="https://wuhaolin.cn/img/icon_wechat.png" width="0" height="0"> -->
<!-- <img src="{{ site.baseurl }}/{% if page.header-img %}{{ page.header-img }}{% else %}{{ site.header-img }}{% endif %}" width="0" height="0"> -->

<!-- Post Header -->
<style type="text/css">
    header.intro-header{
        background-image: url('/img/bg.jpg')
    }
</style>
<header class="intro-header" >
    <div class="container">
        <div class="row">
            <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
                <div class="post-heading">
                    <div class="tags">
                        
                          <a class="tag" href="/tags/#chrome" title="chrome">chrome</a>
                        
                    </div>
                    <h1>使用HeadlessChrome做单页应用SEO</h1>
                    <h2 class="subheading"></h2>
                    <span class="meta">
                        Posted by 吴浩麟 on
                        2017-06-27
                    </span>
                </div>
            </div>
        </div>
    </div>
</header>

<!-- Post Content -->
<article>
    <div class="container">
        <div class="row">
    <!-- Post Container -->
            <div class="
                col-lg-8 col-lg-offset-2
                col-md-10 col-md-offset-1
                post-container">

                <blockquote>
<p>随着react、vue、angular等前端框架的流行越来越多的web应用变成了单页应用，它们的特点是异步拉取数据在浏览器中渲染出HTML。使用这些框架极大的提升web用户体验和开发效率的同时缺带来一个新问题，那就是这样的网页无法被搜索引擎收录。虽然这些web框架支持服务端渲染，但这可能又会增加开发成本。</p>
</blockquote>
<p>有没有一个可用于任何单页应用的SEO解决方案，让我们不用对代码做改变保持原有的开发效率？<a href="https://github.com/gwuhaolin/chrome-render" target="_blank" rel="noopener">chrome-render</a>可以帮我们做到这点，它通过控制HeadlessChrome渲染出最终的HTML返回给爬虫来实现。</p>
<h1 id="HeadlessChrome介绍"><a href="#HeadlessChrome介绍" class="headerlink" title="HeadlessChrome介绍"></a>HeadlessChrome介绍</h1><p>前不久chrome团队宣布chrome<a href="https://developers.google.com/web/updates/2017/04/headless-chrome" target="_blank" rel="noopener">支持headless模式</a>，HeadlessChrome支持chrome所具有的所有功能只不过因为不显示界面而更快资源占用更小。相比于之前的<a href="http://phantomjs.org" target="_blank" rel="noopener">phantomjs</a>(作者因为HeadlessChrome的推出而宣布停止维护)chrome的优势在于它又一个很强的爹(google)会一直维护它优化它，并且chrome在用户量、体验、速度、稳定性都是第一的，所以我认为HeadlessChrome会渐渐替代之前所有的HeadlessBrowser方案。</p>
<h3 id="如何操控HeadlessChrome"><a href="#如何操控HeadlessChrome" class="headerlink" title="如何操控HeadlessChrome"></a>如何操控HeadlessChrome</h3><p>既然HeadlessChrome是以无界面模式运行的，那要怎么控制它和它交互？<br>chrome提供了远程控制接口，目前可以通过<a href="https://github.com/cyrus-and/chrome-remote-interface" target="_blank" rel="noopener">chrome-remote-interface</a>来用js代码向chrome发送命令进行交互。在启动chrome的时候要开启远程控制接口，然后通过 chrome-remote-interface 连接到chrome后再通过协议控制chrome。具体操作见文档：</p>
<ul>
<li><a href="https://developers.google.com/web/updates/2017/04/headless-chrome" target="_blank" rel="noopener">以headless模式和远程控制模式启动chrome</a></li>
<li><a href="https://github.com/cyrus-and/chrome-remote-interface#sample-api-usage" target="_blank" rel="noopener">连接到远程chrome控制它</a></li>
<li><a href="https://chromedevtools.github.io/devtools-protocol/" target="_blank" rel="noopener">控制chrome时支持哪些操作具体怎么用</a></li>
</ul>
<h1 id="chrome-render原理与实践"><a href="#chrome-render原理与实践" class="headerlink" title="chrome-render原理与实践"></a>chrome-render原理与实践</h1><h3 id="原理"><a href="#原理" class="headerlink" title="原理"></a>原理</h3><p>chrome-render先会通过<a href="https://github.com/gwuhaolin/chrome-runner" target="_blank" rel="noopener">chrome-runner</a>以headless模式启动和守护你操作上的chrome，再通过chrome-remote-interface操控chrome去访问需要被SEO的网页让chrome运行这个网页，等到包含数据的HTML被渲染出来时读取当前网页DOM转换成字符串后返回。</p>
<p>怎么知道你的网页什么时候已经渲染出包含数据的HTML了可以返回了呢？为了提升chrome-render效率，默认会在<code>domContentEventFired</code>时返回。对于复杂的场景还可以通过开启chrome-render的<code>useReady</code>选项，等到网页里调用了<code>window.chromeRenderReady()</code>时返回。</p>
<p>只渲染出了HTML还不够我们还需要检测出来着搜索引擎爬虫的访问，如果请求来着爬虫就返回chrome-render渲染后的HTML否则返回正常的单页应用所需HTML。</p>
<p>综上，整体架构如下：<br><img src="https://github.com/gwuhaolin/koa-seo/raw/master/doc/koa-seo%20arch.png" alt="koa-seo arch"></p>
<h3 id="实践"><a href="#实践" class="headerlink" title="实践"></a>实践</h3><p>只需以下几行简单代码就可让chrome渲染出HTML：<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> ChromeRender = <span class="built_in">require</span>(<span class="string">'chrome-render'</span>);</span><br><span class="line">ChromeRender.new().then(<span class="keyword">async</span>(chromeRender)=&gt;&#123;</span><br><span class="line">    <span class="keyword">const</span> htmlString = <span class="keyword">await</span> chromeRender.render(&#123;</span><br><span class="line">       url: <span class="string">'http://qq.com'</span>,</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure></p>
<p>chrome-render只是做了渲染出HTML的工作，要实现SEO还需要和web服务器集成。为了方便大家使用我做了一个koa中间件<a href="https://github.com/gwuhaolin/koa-seo" target="_blank" rel="noopener">koa-seo</a>，要集成到你现有的项目很简单，如下：<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> seoMiddleware = <span class="built_in">require</span>(<span class="string">'koa-seo'</span>);</span><br><span class="line"><span class="keyword">const</span> app = <span class="keyword">new</span> Koa();</span><br><span class="line">app.use(seoMiddleware());</span><br></pre></td></tr></table></figure></p>
<p>只需像这样接入一个中间件你的单页应用就被SEO了。</p>
<h1 id="应用场景扩展"><a href="#应用场景扩展" class="headerlink" title="应用场景扩展"></a>应用场景扩展</h1><p>chrome-render除了用于通用SEO解决方案其实可以用于通用服务端渲染，因为目的都是渲染出最终的HTML再返回。针对通用服务端渲染我也做了一个koa中间件<a href="https://github.com/gwuhaolin/koa-chrome-render" target="_blank" rel="noopener">koa-chrome-render</a>。使用chrome-render做服务端渲染的</p>
<p>优势在于：</p>
<ul>
<li>通用，适用于所有单页应用</li>
<li>对原有代码几乎无改动，最多再合适的地方加个<code>window.chromeRenderReady()</code>，保持原有开发效率</li>
</ul>
<p>缺点在于：</p>
<ul>
<li>和react、vue等只带的服务端渲染相比性能低（经我测试大约 200ms vs 60ms）</li>
<li>chrome-render渲染时占用资源高，一次渲染大约占用25Mb内存，当请求量大时服务器可能扛不住。但是可以通过缓存渲染结果优化。</li>
</ul>
<h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>大家可能会说这个很像<a href="https://prerender.io" target="_blank" rel="noopener">prerender.io</a>，没错思路是一样的，chrome-render的优势在于：</p>
<ul>
<li>chrome-render开源可自己部署，prerender要收费是商业产品</li>
<li>prerender基于已经停止维护的phantomjs</li>
</ul>
<p>本文中所提到的相关项目都是开源的并且有详细的使用文档，它们的文档链接如下：</p>
<ul>
<li><a href="https://github.com/gwuhaolin/chrome-render" target="_blank" rel="noopener">chrome-render</a></li>
<li><a href="https://github.com/gwuhaolin/chrome-runner" target="_blank" rel="noopener">chrome-runner</a></li>
<li><a href="https://github.com/gwuhaolin/koa-seo" target="_blank" rel="noopener">koa-seo</a></li>
<li><a href="https://github.com/gwuhaolin/koa-chrome-render" target="_blank" rel="noopener">koa-chrome-render</a></li>
</ul>
<p>喜欢的给个star，希望大家和我一起来改进它们让它们更强大。</p>
<p><a href="http://wuhaolin.cn/2017/06/27/%E4%BD%BF%E7%94%A8HeadlessChrome%E5%81%9A%E5%8D%95%E9%A1%B5%E5%BA%94%E7%94%A8SEO/">阅读原文</a></p>


                <hr>

                 <strong><a href="https://github.com/gwuhaolin/blog/issues/8" target="_blank" rel="noopener">在Github上参与本文讨论</a></strong>

                <ul class="pager">
                    
                        <li class="previous">
                            <a href="/2017/08/01/流媒体加密/" data-toggle="tooltip" data-placement="top" title="流媒体加密">&larr; Previous Post</a>
                        </li>
                    
                    
                        <li class="next">
                            <a href="/2017/06/25/基于HeadlessChrome的网页自动化测试系统-FinalTest/" data-toggle="tooltip" data-placement="top" title="基于HeadlessChrome的网页自动化测试系统-FinalTest">Next Post &rarr;</a>
                        </li>
                    
                </ul>
            </div>
    <!-- Side Catalog Container -->
        

    <!-- Sidebar Container -->

            <div class="
                col-lg-8 col-lg-offset-2
                col-md-10 col-md-offset-1
                sidebar-container">

                <!-- Featured Tags -->
                
                <section>
                    <!-- no hr -->
                    <h5><a href="/tags/">FEATURED TAGS</a></h5>
                    <div class="tags">
                       
                          <a class="tag" href="/tags/#chrome" title="chrome">chrome</a>
                        
                    </div>
                </section>
                

                <!-- Friends Blog -->
                
            </div>

        </div>
    </div>
</article>


<!-- async load function -->
<script>
    function async(u, c) {
      var d = document, t = 'script',
          o = d.createElement(t),
          s = d.getElementsByTagName(t)[0];
      o.src = u;
      if (c) { o.addEventListener('load', function (e) { c(null, e); }, false); }
      s.parentNode.insertBefore(o, s);
    }
</script>
<!-- anchor-js, Doc:http://bryanbraun.github.io/anchorjs/ -->
<script>
    async("https://cdn.bootcss.com/anchor-js/1.1.1/anchor.min.js",function(){
        anchors.options = {
          visible: 'always',
          placement: 'right',
          icon: '#'
        };
        anchors.add().remove('.intro-header h1').remove('.subheading').remove('.sidebar-container h5');
    })
</script>
<style>
    /* place left on bigger screen */
    @media all and (min-width: 800px) {
        .anchorjs-link{
            position: absolute;
            left: -0.75em;
            font-size: 1.1em;
            margin-top : -0.1em;
        }
    }
</style>



<!-- Footer -->
<!-- Footer -->
<footer>
    <div class="container">
        <div class="row">
            <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
                <ul class="list-inline text-center">
                    
                    
                    
                    <li>
                        <a target="_blank" href="https://www.zhihu.com/people/wu-hao-lin-67-15">
                            <span class="fa-stack fa-lg">
                                <i class="fa fa-circle fa-stack-2x"></i>
                                <i class="fa  fa-stack-1x fa-inverse">知</i>
                            </span>
                        </a>
                    </li>
                    

                    

                    

                    
                    <li>
                        <a target="_blank" href="https://github.com/gwuhaolin">
                            <span class="fa-stack fa-lg">
                                <i class="fa fa-circle fa-stack-2x"></i>
                                <i class="fa fa-github fa-stack-1x fa-inverse"></i>
                            </span>
                        </a>
                    </li>
                    

                    

                </ul>
                <p class="copyright text-muted">
                    Copyright &copy; 浩麟的博客 2019
                    <br>
                    Ported by <a href="https://github.com/gwuhaolin" target="_blank" rel="noopener">gwuhaolin</a> |
                    订阅本站<iframe
                            style="margin-left: 2px; margin-bottom:-5px;"
                            frameborder="0" scrolling="0" width="91px" height="20px"
                            src="https://ghbtns.com/github-btn.html?user=gwuhaolin&repo=blog&type=star&count=true">
                    </iframe>
                </p>
            </div>
        </div>
    </div>
</footer>

<!-- jQuery -->

<script src="/js/jquery.min.js"></script>


<!-- Bootstrap Core JavaScript -->

<script src="/js/bootstrap.min.js"></script>


<!-- Custom Theme JavaScript -->

<script src="/js/hux-blog.min.js"></script>



<!-- async load function -->
<script>
    function async(u, c) {
        var d = document, t = 'script',
            o = d.createElement(t),
            s = d.getElementsByTagName(t)[0];
        o.src = u;
        if (c) {
            o.addEventListener('load', function (e) {
                c(null, e);
            }, false);
        }
        s.parentNode.insertBefore(o, s);
    }
</script>


<!-- jquery.tagcloud.js -->
<script>
    // only load tagcloud.js in tag.html
    if ($('#tag_cloud').length !== 0) {
        async("https://wuhaolin.cn/js/jquery.tagcloud.js", function () {
            $.fn.tagcloud.defaults = {
                //size: {start: 1, end: 1, unit: 'em'},
                color: {start: '#bbbbee', end: '#0085a1'},
            };
            $('#tag_cloud a').tagcloud();
        })
    }
</script>

<!--fastClick.js -->
<script>
    async("https://cdn.bootcss.com/fastclick/1.0.6/fastclick.min.js", function () {
        var $nav = document.querySelector("nav");
        if ($nav) FastClick.attach($nav);
    })
</script>


<!-- Google Analytics -->


<script>
    // dynamic User by Hux
    var _gaId = 'UA-54964850-1';
    var _gaDomain = 'wuhaolin.cn';

    // Originial
    (function (i, s, o, g, r, a, m) {
        i['GoogleAnalyticsObject'] = r;
        i[r] = i[r] || function () {
            (i[r].q = i[r].q || []).push(arguments)
        }, i[r].l = 1 * new Date();
        a = s.createElement(o),
            m = s.getElementsByTagName(o)[0];
        a.async = 1;
        a.src = g;
        m.parentNode.insertBefore(a, m)
    })(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');

    ga('create', _gaId, _gaDomain);
    ga('send', 'pageview');
</script>




<!-- Side Catalog -->


<!--Google Auto AD-->
<script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<script>
    (adsbygoogle = window.adsbygoogle || []).push({
        google_ad_client: "ca-pub-9697944574373240",
        enable_page_level_ads: true
    });
</script>



<!-- Image to hack wechat -->
<img src="https://wuhaolin.cn /img/icon_wechat.png" width="0" height="0"/>
<!-- Migrate from head to bottom, no longer block render and still work -->
</body>

</html>
